﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Renci.iRods.Common;
using Renci.iRods.Packets;
using Renci.iRods.Exceptions;

namespace Renci.iRods
{
    public class IRodsStream : Stream
    {
        private object _lock = new object();

        private IRodsConnection _connection;

        private long _length;

        private bool _canRead;

        private bool _canWite;

        public IRodsStream(IRodsConnection connection, string path, FileMode mode, FileAccess access)
        {
            if (connection == null)
            {
                throw new ArgumentNullException("connection");
            }

            if (!connection.IsConnected)
                throw new IRodsConnectionException("Not connected.");

            this._connection = connection;

            this.Name = path;

            RequestCodes requestCode = RequestCodes.None;

            //  Open file
            var request = new DataObjectInputPacket(path);

            switch (access)
            {
                case FileAccess.Read:
                    request.OpenFlag = 0;
                    this._canRead = true;
                    this._canWite = false;
                    break;
                case FileAccess.ReadWrite:
                    request.OpenFlag = 2;
                    this._canRead = true;
                    this._canWite = true;
                    break;
                case FileAccess.Write:
                    request.OpenFlag = 1;
                    this._canRead = false;
                    this._canWite = true;
                    break;
                default:
                    break;
            }

            switch (mode)
            {
                case FileMode.CreateNew:
                    //  Create new, override if file exists
                    request.AddKey("forceFlag", string.Empty);
                    goto case FileMode.Create;
                case FileMode.Create:
                    //  Create new, throw exception if file exists
                    requestCode = RequestCodes.DATA_OBJ_CREATE_AND_STAT_AN;
                    request.CreateMode = 33188;
                    request.Offset = 0;
                    request.DataSize = -1;
                    request.NumOfThreads = 0;

                    request.AddKey("dataType", "generic");
                    request.AddKey("rescName", this._connection.ResourceName);
                    this.Position = 0;
                    break;
                case FileMode.Open:
                case FileMode.Append:
                case FileMode.OpenOrCreate:
                    //  Open file or create a new one if one doesn't exists
                    requestCode = RequestCodes.DATA_OBJ_OPEN_AND_STAT_AN;
                    this.Position = 0;
                    break;
                case FileMode.Truncate:
                    this.Position = 0;
                    break;
                default:
                    break;
            }

            this._connection.SendRequest(requestCode, request);

            var response = this._connection.ReceiveMessage<OpenStatusPacket>();

            //  When trying to open file that doesn't exists then create a new one
            if (mode == FileMode.OpenOrCreate && response.Exception.ErrorCode == ErrorCodes.CAT_NO_ROWS_FOUND)
            {
                requestCode = RequestCodes.DATA_OBJ_CREATE_AND_STAT_AN;
                request.CreateMode = 33188;
                request.Offset = 0;
                request.DataSize = -1;
                request.NumOfThreads = 0;

                request.AddKey("dataType", "generic");

                this._connection.SendRequest(requestCode, request);

                response = this._connection.ReceiveMessage<OpenStatusPacket>();
            }

            if (response.Exception != null)
            {
                throw response.Exception;
            }

            this._length = (long)response.Body.Size;
            this.DataType = response.Body.DataType;
            this.DataMode = response.Body.DataMode;
            this.Handle = response.Body.Handle;
            this.ReplicationStatus = response.Body.ReplicationStatus;
            this.ResourceID = response.Body.ResourceID;

            //  If append mode then set position to file length
            if (mode == FileMode.Append)
            {
                this.Position = this._length;
            }
        }

        public string Name { get; private set; }

        public int Handle { get; private set; }

        public override bool CanRead
        {
            get { return this._canRead; }
        }

        public override bool CanSeek
        {
            get { return true; }
        }

        public override bool CanWrite
        {
            get { return this._canWite; }
        }

        public override void Flush()
        {
            throw new NotImplementedException();
        }

        public override long Length
        {
            get { return this._length; }
        }

        public override long Position { get; set; }

        public string DataType { get; private set; }

        public string DataMode { get; private set; }

        public int ReplicationStatus { get; private set; }

        public int ResourceID { get; private set; }

        public override int Read(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");

            if (offset < 0)
                throw new ArgumentOutOfRangeException("offset");

            if (count < 0)
                throw new ArgumentOutOfRangeException("count");

            if (buffer.Length - offset < count)
                throw new ArgumentException("Invalid offset length.");

            var bytesRead = 0;

            lock (_lock)
            {
                var readRequest = new OpenedDataObjectInputPacket();
                readRequest.Offset = 0;
                readRequest.Length = count;
                readRequest.Handle = this.Handle;

                this._connection.SendRequest(RequestCodes.DATA_OBJ_READ_AN, readRequest);

                var response = this._connection.ReceiveMessage();

                if (response.Exception != null)
                {
                    throw response.Exception;
                }

                if (response.BinaryData != null)
                {
                    bytesRead = response.BinaryData.Read(buffer, offset, count);

                    this.Position += bytesRead;
                }
            }

            return bytesRead;
        }

        public override long Seek(long offset, SeekOrigin origin)
        {
            var readRequest = new OpenedDataObjectInputPacket();
            readRequest.Offset = offset;
            readRequest.Handle = this.Handle;
            readRequest.SeekOrigin = origin;

            this._connection.SendRequest(RequestCodes.DATA_OBJ_LSEEK_AN, readRequest);

            var response = this._connection.ReceiveMessage<FileLSeekOutputPacket>();

            if (response.Exception != null)
            {
                throw response.Exception;
            }

            this.Position = (long)response.Body.Offset;

            return this.Position;
        }

        public override void SetLength(long value)
        {
            if (value < 0)
                throw new ArgumentOutOfRangeException("value");

            if (this.Handle == 0)
                throw new InvalidOperationException("File not open.");            

            if (!this.CanSeek)
                throw new NotSupportedException("Seek operation is not supported.");

            if (!this.CanWrite)
                throw new NotSupportedException("Write operation is not supported.");

            this._length = value;

            if (this.Position > value)
                this.Position = value;
        }

        public override void Write(byte[] buffer, int offset, int count)
        {
            if (buffer == null)
                throw new ArgumentNullException("buffer");

            if (offset < 0)
                throw new ArgumentOutOfRangeException("offset");

            if (count < 0)
                throw new ArgumentOutOfRangeException("count");

            if (buffer.Length - offset < count)
                throw new ArgumentException("Invalid offset length.");

            lock (_lock)
            {
                var writeRequest = new OpenedDataObjectInputPacket();
                writeRequest.Offset = 0;
                writeRequest.Length = count;
                writeRequest.Handle = this.Handle;

                using (var ms = new MemoryStream(buffer, offset, count))
                {
                    this._connection.SendRequest(RequestCodes.DATA_OBJ_WRITE_AN, writeRequest, ms);
                }

                var response = this._connection.ReceiveMessage();

                if (response.Exception != null)
                {
                    throw response.Exception;
                }

                this.Position += count;

                if (this._length < this.Position)
                {
                    this._length = this.Position;
                }
            }
        }

        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);

            //  Close file handle
            var request = new OpenedDataObjectInputPacket();
            request.Handle = this.Handle;
            this._connection.SendRequest(RequestCodes.DATA_OBJ_CLOSE_AN, request);

            var response = this._connection.ReceiveMessage();

            if (response.Exception != null)
            {
                throw response.Exception;
            }
        }
    }
}
